package com.example.simplesvc.concurrent.lock;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.*;
import java.util.concurrent.TimeUnit;

public class DLock {

    private SqlSessionFactory ssf;

    private String name;
    private int maxWait = 18;
    private int waitInterval = 5;

    private static Logger log = LoggerFactory.getLogger(DLock.class);

    public DLock(SqlSessionFactory ssf, String name, int maxWait,int waitInterval) {
        this.ssf =ssf;
        this.name = name;
        this.maxWait = maxWait;
        this.waitInterval = waitInterval;
    }

    public DLock(SqlSessionFactory ssf, String name) {
        this.ssf =ssf;
        this.name = name;
    }

    /**
     * @param expirePeriod
     */
    public void lock(long expirePeriod) {
         LockData lockData = new LockData();
        SqlSession session = ssf.openSession();


        try (Connection connection = session.getConnection()){
            loadLockData(connection, lockData);
            PreparedStatement pps;
            boolean locked = false;
            if (lockData.getId().longValue() == -1L) {//没有加载到锁信息 插入锁
                try {
                    pps = connection
                            .prepareStatement("insert into dlock(name,version,status,locked_time,expire_time,created_time,owner) values(?,?,?,?,?,?,?)");
                    pps.setString(1, name);
                    pps.setLong(2, 0L);
                    pps.setInt(3, 0);
                    long now = System.currentTimeMillis();
                    pps.setDate(4, new java.sql.Date(now));
                    pps.setDate(5, new java.sql.Date(now + expirePeriod));
                    pps.setDate(6, new java.sql.Date(now));
                    pps.setString(7, Thread.currentThread().getName());

                    locked = pps.execute();
                } catch (SQLException e) {
                    //可能会引发唯一索引冲突，报错说明已经存在锁， 重新加载
                    locked = false;

                }
            }

            if (locked) {
                return;
            }
            for (int i = 0; i < maxWait; i++) {
                loadLockData(connection, lockData);
               // log.info("try to lock@ {}", Thread.currentThread().getName());
                locked = tryLock(expirePeriod, connection, lockData);
                if (locked) {
                    return;
                }
                //没有上锁成功，等待10秒之后继续抢锁
                try {
                    Thread.sleep(TimeUnit.SECONDS.toMillis(waitInterval));
                } catch (InterruptedException e) {
                    //
                }
            }

            //最后仍没有等到锁
            throw new RuntimeException("Failed to get lock");

        } catch (SQLException sq) {
            throw new RuntimeException("Failed to get lock", sq);
        }

    }

    protected boolean tryLock(long expirePeriod, Connection connection, LockData lockData) throws SQLException {
        PreparedStatement pps;
        boolean locked;
        pps = connection.prepareStatement("update dlock set version=?,status=1,locked_time=?,expire_time=?,owner=? where name=? and version=? and (status=0 or (status=1 and expire_time <now()))");
        long nextVersion = lockData.getVersion() + 1;
        pps.setLong(1, nextVersion);
        long now = System.currentTimeMillis();
        pps.setTimestamp(2, new Timestamp(now));
        pps.setTimestamp(3, new Timestamp(now + expirePeriod));
        pps.setString(4, Thread.currentThread().getName());
        pps.setString(5, name);
        pps.setLong(6, lockData.getVersion());
        int c = pps.executeUpdate();
        if( c == 1) {
            log.info("加锁成功 {}", lockData);
            lockData.setVersion(nextVersion);
            return true;
        }
        return false;
    }

    private void loadLockData(Connection connection,LockData lockData) throws SQLException {
        PreparedStatement pps = connection
                .prepareStatement("select id,version,status ,expire_time, locked_time from dlock where name=?");
        pps.setString(1, name);
        ResultSet rs = pps.executeQuery();
        while (rs.next()) {
            lockData.setId(rs.getLong("id"));
            lockData.setStatus(rs.getInt("status"));
            lockData.setVersion(rs.getLong("version"));
            lockData.setName(name);
            lockData.setExpireTime(rs.getTimestamp("expire_time"));
            lockData.setLockedTime(rs.getTimestamp("locked_time"));
        }
    }

    public void unlock() {
        SqlSession session = ssf.openSession();
        LockData lockData = new LockData();
        try ( Connection connection = session.getConnection();){
            loadLockData(connection,lockData);
            PreparedStatement pps = connection.prepareStatement("update dlock set version=?,status=0,owner=null where name=? and version=? and status=1");
            pps.setLong(1, lockData.getVersion() + 1);
            pps.setString(2, name);
            pps.setLong(3, lockData.getVersion());
            int i = pps.executeUpdate();
            if (i < 1) {
                //
                log.warn("锁已被释放{}",lockData);
            } else {
                log.debug("释放锁成功{}", lockData);
            }


        } catch (SQLException e) {
            throw new RuntimeException("解锁失败", e);
        }
    }
}
